/************************************************************************
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * Copyright 2008, 2010 Oracle and/or its affiliates. All rights reserved.
 * 
 * Use is subject to license terms.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0. You can also
 * obtain a copy of the License at http://odftoolkit.org/docs/license.txt
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * 
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ************************************************************************/
package org.odftoolkit.odfvalidator;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Iterator;
import java.util.Set;
import java.util.zip.ZipException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.sax.SAXSource;
import javax.xml.validation.Validator;
import org.odftoolkit.odfdom.doc.OdfDocument;
import org.odftoolkit.odfdom.pkg.OdfPackage;
import org.odftoolkit.odfdom.pkg.manifest.EncryptionDataElement;
import org.odftoolkit.odfdom.pkg.manifest.OdfFileEntry;
import org.xml.sax.InputSource;
import org.xml.sax.XMLFilter;
import org.xml.sax.XMLReader;

import org.xml.sax.helpers.DefaultHandler;

/**
 * Validator for Files
 */
abstract class ODFPackageValidator {

	static final String DOCUMENT_SETTINGS = "document-settings";
	static final String DOCUMENT_STYLES = "document-styles";
	static final String DOCUMENT_CONTENT = "document-content";
	protected Logger.LogLevel m_nLogLevel;
	protected OdfValidatorMode m_eMode = OdfValidatorMode.CONFORMANCE;
	protected SAXParseExceptionFilter m_aFilter = null;
	protected ODFValidatorProvider m_aValidatorProvider = null;
	protected ODFValidationResult m_aResult = null;
	protected OdfVersion m_aConfigVersion = null;
	private SAXParserFactory m_aSAXParserFactory = null;
	protected OdfVersion mOdfPackageVersion = null;

	protected ODFPackageValidator(Logger.LogLevel nLogLevel, OdfValidatorMode eMode, OdfVersion aVersion,
			SAXParseExceptionFilter aFilter, ODFValidatorProvider aValidatorProvider) {
		m_nLogLevel = nLogLevel;
		m_eMode = eMode;
		m_aFilter = aFilter;
		m_aValidatorProvider = aValidatorProvider;
		m_aConfigVersion = aVersion;
		m_aResult = new ODFValidationResult(aVersion, eMode);
	}

	protected abstract String getLoggerName();

	protected abstract String getDocumentPath();

	protected abstract OdfPackage getPackage(Logger aLogger);

	protected abstract String getStreamName(String aEntry);

	protected boolean validate(PrintStream aOut) throws ODFValidatorException {
		Logger aLogger =
				new Logger(getLoggerName(), getDocumentPath(), aOut, m_nLogLevel);
		return _validate(aLogger);
	}

	protected boolean validate(Logger aParentLogger) throws ODFValidatorException {
		Logger aLogger =
				new Logger(getDocumentPath(), aParentLogger);
		return _validate(aLogger);
	}

	
	OdfVersion getOdfPackageVersion(){
		return mOdfPackageVersion;
	}
	
	private boolean _validate(Logger aLogger) throws ODFValidatorException {
		boolean bHasErrors = false;

		OdfPackage aPkg = getPackage(aLogger);
		if (aPkg == null) {
			return true;
		}

		try {
			String aDocVersion = getVersion(aLogger);
			if (aDocVersion != null) {
				aLogger.logInfo("ODF version of root document: " + aDocVersion, false);
				mOdfPackageVersion = OdfVersion.valueOf(aDocVersion, true);
			}
			OdfVersion aVersion = m_aConfigVersion == null ? OdfVersion.valueOf(aDocVersion, true) : m_aConfigVersion;

			bHasErrors |= validatePre(aLogger, aVersion);
			aLogger.logInfo("Media Type: " + m_aResult.getMediaType(), false);

			bHasErrors |= validateMeta(aLogger, getStreamName(OdfDocument.OdfXMLFile.META.getFileName()), aVersion, true);
			bHasErrors |= validateEntry(aLogger, getStreamName(OdfDocument.OdfXMLFile.SETTINGS.getFileName()), DOCUMENT_SETTINGS, aVersion);
			bHasErrors |= validateEntry(aLogger, getStreamName(OdfDocument.OdfXMLFile.STYLES.getFileName()), DOCUMENT_STYLES, aVersion);
			if (m_aResult.getMediaType().equals(ODFMediaTypes.FORMULA_MEDIA_TYPE)) {
				bHasErrors |= validateMathML(aLogger, getStreamName(OdfDocument.OdfXMLFile.CONTENT.getFileName()), aVersion);
			} else {
				bHasErrors |= validateEntry(aLogger, getStreamName(OdfDocument.OdfXMLFile.CONTENT.getFileName()), DOCUMENT_CONTENT, aVersion);
			}
			bHasErrors |= validatePost(aLogger, aVersion);
		} catch (ZipException e) {
			aLogger.logFatalError(e.getMessage());
		} catch (IOException e) {
			aLogger.logFatalError(e.getMessage());
		}

		logSummary(bHasErrors, aLogger);

		return bHasErrors || aLogger.hasError();
	}

	protected boolean validatePre(Logger aLogger, OdfVersion aVersion) throws ODFValidatorException, IOException {
		return false;
	}

	protected boolean validatePost(Logger aLogger, OdfVersion aVersion) throws ODFValidatorException, IOException {
		return false;
	}

	protected void logSummary(boolean bHasErrors, Logger aLogger) {
	}

	protected boolean validateEntry(Logger aParentLogger, String aEntryName, String aLocalElementName, OdfVersion aVersion) throws IOException, ZipException, IllegalStateException, ODFValidatorException {
		Logger aLogger = new Logger(aEntryName, aParentLogger);
		XMLFilter aFilter = new ContentFilter(aLogger, aLocalElementName);
		if ((m_eMode == OdfValidatorMode.CONFORMANCE && aVersion.compareTo(OdfVersion.V1_1) <= 0)
				|| m_eMode == OdfValidatorMode.EXTENDED_CONFORMANCE) {
			XMLFilter aAlienFilter = new ForeignContentFilter(aLogger, aVersion, m_aResult);
			aAlienFilter.setParent(aFilter);
			aFilter = aAlienFilter;
		}
		Validator aValidator = null;
		if (m_eMode == OdfValidatorMode.VALIDATE_STRICT) {
			aValidator = m_aValidatorProvider.getStrictValidator(aParentLogger.getOutputStream(), aVersion);
		} else {
			aValidator = m_aValidatorProvider.getValidator(aParentLogger.getOutputStream(), aVersion);
		}
//		Validator aValidator = m_eMode == OdfValidatorMode.VALIDATE_STRICT ? m_aValidatorProvider.getStrictValidator(aParentLogger.getOutputStream(), aVersion)
//				: m_aValidatorProvider.getValidator(aParentLogger.getOutputStream(), aVersion);
		return validateEntry(aFilter, aValidator, aLogger, aEntryName);
	}

	private boolean validateMeta(Logger aParentLogger, String aEntryName, OdfVersion aVersion, boolean bIsRoot) throws IOException, ZipException, IllegalStateException, ODFValidatorException {
		Logger aLogger = new Logger(aEntryName, aParentLogger);
		XMLFilter aFilter = new MetaFilter(aLogger, m_aResult);
		if ((m_eMode == OdfValidatorMode.CONFORMANCE && aVersion.compareTo(OdfVersion.V1_1) <= 0)
				|| m_eMode == OdfValidatorMode.EXTENDED_CONFORMANCE) {
			XMLFilter aAlienFilter = new ForeignContentFilter(aLogger, aVersion, m_aResult);
			aAlienFilter.setParent(aFilter);
			aFilter = aAlienFilter;
		}

		Validator aValidator = null;
		if (m_eMode == OdfValidatorMode.VALIDATE_STRICT) {
			aValidator = m_aValidatorProvider.getStrictValidator(aParentLogger.getOutputStream(), aVersion);
		} else {
			aValidator = m_aValidatorProvider.getValidator(aParentLogger.getOutputStream(), aVersion);
		}
		return validateEntry(aFilter, aValidator, aLogger, aEntryName);
	}

	private boolean validateMathML(Logger aParentLogger, String aEntryName, OdfVersion aVersion) throws IOException, ZipException, IllegalStateException, ODFValidatorException {
		Logger aLogger = new Logger(aEntryName, aParentLogger);
		String aMathMLDTDSystemId = m_aValidatorProvider.getMathMLDTDSystemId(aVersion);
		if (aMathMLDTDSystemId != null) {
			// validate using DTD
			return parseEntry(new MathML101Filter(aMathMLDTDSystemId, aLogger), aLogger, aEntryName, true);
		} else {
			Validator aMathMLValidator = m_aValidatorProvider.getMathMLValidator(aParentLogger.getOutputStream(), null);
			if (aMathMLValidator == null) {
				aLogger.logInfo("MathML schema is not available. Validation has been skipped.", false);
				return false;
			}
			return validateEntry(new MathML20Filter(aLogger), aMathMLValidator, aLogger, aEntryName);
		}
	}

	protected boolean validateDSig(Logger aParentLogger, String aEntryName, OdfVersion aVersion) throws IOException, ZipException, IllegalStateException, ODFValidatorException {
		Validator aValidator = m_aValidatorProvider.getDSigValidator(aParentLogger.getOutputStream(), aVersion);
		Logger aLogger = new Logger(aEntryName, aParentLogger);
		if (aValidator == null) {
			aLogger.logWarning("Signature not validated because there is no Signature Validator configured for the selected Configuration");
			return false;
		}

		return validateEntry(new DSigFilter(aLogger), aValidator, aLogger, aEntryName);
	}

	protected boolean validateEntry(XMLFilter aFilter,
			Validator aValidator, Logger aLogger,
			String aEntryName) throws IOException, ZipException, IllegalStateException, ODFValidatorException {
		OdfPackage aPkg = getPackage(aLogger);

		if (!aEntryName.equals(OdfPackage.OdfFile.MANIFEST.getPath()) && isEncrypted(aEntryName, aLogger)) {
			return false;
		}

		InputStream aInStream = null;
		try {
			aInStream = aPkg.getInputStream(aEntryName, true);
            aLogger.setInputStream(aPkg.getInputStream(aEntryName, true));
		} catch (Exception e) {
			throw new ODFValidatorException(e);
		}


		if (aValidator == null) {

			aLogger.logWarning("no Validator configured in selected Configuration for this file type");
			return false;
		}

		return aInStream != null ? validate(aInStream, aFilter, aValidator, aLogger) : false;
	}

	private boolean validate(InputStream aInStream,
			XMLFilter aFilter,
			javax.xml.validation.Validator aValidator,
			Logger aLogger) throws ODFValidatorException {
		SAXParser aParser = getSAXParser(false);
		SchemaErrorHandler aErrorHandler = new SchemaErrorHandler(aLogger, m_aFilter);

		try {
			XMLReader aReader;
			if (aFilter != null) {
				XMLReader aParent = aFilter.getParent();
				if (aParent != null) {
					((XMLFilter) aParent).setParent(aParser.getXMLReader());
				} else {
					aFilter.setParent(aParser.getXMLReader());
				}
				aReader = aFilter;
			} else {
				aReader = aParser.getXMLReader();
			}

			if (m_aFilter != null) {
				m_aFilter.startSubFile();
			}
			aValidator.setErrorHandler(aErrorHandler);
			try {
				aValidator.validate(new SAXSource(aReader,
						new InputSource(aInStream)));
			} catch (RuntimeException e) {
				aLogger.logFatalError(e.getMessage());
				m_aValidatorProvider.resetValidatorProvider();
			}
		} catch (org.xml.sax.SAXParseException e) {
			aErrorHandler.fatalErrorNoException(e);
		} catch (org.xml.sax.SAXException e) {
			aLogger.logFatalError(e.getMessage());
		} catch (IOException e) {
			aLogger.logFatalError(e.getMessage());
		}

		aLogger.logSummaryInfo();
		if (m_aResult.hasForeignElements()) {
			Set<String> aForeignElementURISet = m_aResult.getForeignElements().keySet();
			StringBuilder aBuffer = new StringBuilder();
			Iterator<String> aIter = aForeignElementURISet.iterator();
			boolean bFirst = true;
			while (aIter.hasNext()) {
				String aURI = aIter.next();
				aBuffer.setLength(0);
				aBuffer.append(m_aResult.getForeignElements().get(aURI));
				aBuffer.append(" extension elements from the following namespace were found: ");
				aBuffer.append(aURI);
				aLogger.logInfo(aBuffer.toString(), false);
			}
		}
		if (m_aResult.hasForeignAttributes()) {
			Set<String> aForeignAttributeURISet = m_aResult.getForeignAttributes().keySet();
			Iterator<String> aIter = aForeignAttributeURISet.iterator();
			StringBuilder aBuffer = new StringBuilder();
			while (aIter.hasNext()) {
				String aURI = aIter.next();
				aBuffer.setLength(0);
				aBuffer.append(m_aResult.getForeignAttributes().get(aURI));
				aBuffer.append(" extension attributes from the following namespace were found: ");
				aBuffer.append(aURI);
				aLogger.logInfo(aBuffer.toString(), false);
			}
		}
		return aLogger.hasError();
	}

	protected boolean parseEntry(XMLFilter aFilter,
			Logger aLogger,
			String aEntryName, boolean bValidating) throws IOException, ZipException, IllegalStateException, ODFValidatorException {
		OdfPackage aPkg = getPackage(aLogger);

		if (isEncrypted(aEntryName, aLogger)) {
			return false;
		}

		InputStream aInStream = null;
		try {
			aInStream = getPackage(aLogger).getInputStream(aEntryName, true);
		} catch (Exception e) {
			throw new ODFValidatorException(e);
		}

		return aInStream != null ? parse(aInStream, aFilter, bValidating, aLogger) : false;
	}

	private boolean parse(InputStream aInStream, XMLFilter aFilter, boolean bValidating, Logger aLogger) throws ODFValidatorException {
		SAXParser aParser = getSAXParser(bValidating);
		SchemaErrorHandler aErrorHandler = new SchemaErrorHandler(aLogger, m_aFilter);

		try {
			XMLReader aReader;
			if (aFilter != null) {
				aFilter.setParent(aParser.getXMLReader());
				aReader = aFilter;
			} else {
				aReader = aParser.getXMLReader();
			}
			if (m_aFilter != null) {
				m_aFilter.startSubFile();
			}
			aReader.setErrorHandler(aErrorHandler);
			aReader.parse(new InputSource(aInStream));
		} catch (org.xml.sax.SAXParseException e) {
			aErrorHandler.fatalErrorNoException(e);
		} catch (org.xml.sax.SAXException e) {
			aLogger.logFatalError(e.getMessage());
		} catch (IOException e) {
			aLogger.logFatalError(e.getMessage());
		}

		if (bValidating) {
			aLogger.logSummaryInfo();
		}
		return aLogger.hasError();
	}

	private boolean isEncrypted(String aEntryName, Logger aLogger) {
		OdfFileEntry aFileEntry = getPackage(aLogger).getFileEntry(aEntryName);
		if (aFileEntry != null) {
			EncryptionDataElement aEncData = aFileEntry.getEncryptionData();
			if (aEncData != null) {
				aLogger.logFatalError("stream content is encrypted. Validataion of encrypted content is not supported.");
				return true;
			}
		}
		return false;
	}

	private SAXParser getSAXParser(boolean bValidating) throws ODFValidatorException {
		SAXParser aParser = null;
		if (m_aSAXParserFactory == null) {
			m_aSAXParserFactory = SAXParserFactory.newInstance();
			m_aSAXParserFactory.setNamespaceAware(true);
		}

		try {
			m_aSAXParserFactory.setValidating(bValidating);
			aParser = m_aSAXParserFactory.newSAXParser();
		} catch (javax.xml.parsers.ParserConfigurationException e) {
			throw new ODFValidatorException(e);
		} catch (org.xml.sax.SAXException e) {
			throw new ODFValidatorException(e);
		}

		return aParser;
	}

	/**
	 * get the generator
	 */
	public String getGenerator() {
		return m_aResult.getGenerator();
	}

	private String getVersion(Logger aLogger) throws ODFValidatorException {
		String aVersion = null;

		InputStream aInStream = null;
		try {
			OdfPackage aPkg = getPackage(aLogger);
			aInStream = aPkg.getInputStream(getStreamName(OdfDocument.OdfXMLFile.META.getFileName()), true);
			if (aInStream == null) {
				aInStream = aPkg.getInputStream(getStreamName(OdfDocument.OdfXMLFile.SETTINGS.getFileName()), true);
			}
			if (aInStream == null) {
				aInStream = aPkg.getInputStream(getStreamName(OdfDocument.OdfXMLFile.CONTENT.getFileName()), true);
			}
			if (aInStream == null) {
				return null;
			}
		} catch (Exception e) {
			aLogger.logFatalError(e.getMessage());
		}

		SAXParser aParser = getSAXParser(false);

		DefaultHandler aHandler = new VersionHandler();

		try {
			aParser.parse(aInStream, aHandler);
		} catch (SAXVersionException e) {
			aVersion = e.getVersion();
		} catch (org.xml.sax.SAXException e) {
			aLogger.logFatalError(e.getMessage());
		} catch (IOException e) {
			aLogger.logFatalError(e.getMessage());
		}

		return aVersion;
	}
}
